/*
 * (c) Copyright Ascensio System SIA 2010-2024
 *
 * This program is a free software product. You can redistribute it and/or
 * modify it under the terms of the GNU Affero General Public License (AGPL)
 * version 3 as published by the Free Software Foundation. In accordance with
 * Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect
 * that Ascensio System SIA expressly excludes the warranty of non-infringement
 * of any third-party rights.
 *
 * This program is distributed WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR  PURPOSE. For
 * details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
 *
 * You can contact Ascensio System SIA at 20A-6 Ernesta Birznieka-Upish
 * street, Riga, Latvia, EU, LV-1050.
 *
 * The  interactive user interfaces in modified source and object code versions
 * of the Program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU AGPL version 3.
 *
 * Pursuant to Section 7(b) of the License you must retain the original Product
 * logo when distributing the program. Pursuant to Section 7(e) we decline to
 * grant you any rights under trademark law for use of our trademarks.
 *
 * All the Product's GUI elements, including illustrations and icon sets, as
 * well as technical writing content are licensed under the terms of the
 * Creative Commons Attribution-ShareAlike 4.0 International. See the License
 * terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
 *
 */

const {jest, describe, test, expect, beforeAll, afterAll} = require('@jest/globals');
jest.mock('fs/promises', () => ({
  ...jest.requireActual('fs/promises'),
  cp: jest.fn().mockImplementation((from, to) => fs.writeFileSync(to, testFileData3))
}));
const mockNeedServeStatic = jest.fn().mockReturnValue(true);
jest.mock('../../../Common/sources/storage/storage-base', () => {
  const originalModule = jest.requireActual('../../../Common/sources/storage/storage-base');
  return {
    ...originalModule,
    needServeStatic: mockNeedServeStatic
  };
});
const {cp} = require('fs/promises');
const http = require('http');
const https = require('https');
const fs = require('fs');
const {Readable} = require('stream');

const testFileData1 = 'test1';
const testFileData2 = 'test22';
const testFileData3 = 'test333';
const testFileData4 = testFileData3;

const express = require('express');
const operationContext = require('../../../Common/sources/operationContext');
const tenantManager = require('../../../Common/sources/tenantManager');
const storage = require('../../../Common/sources/storage/storage-base');
const utils = require('../../../Common/sources/utils');
const commonDefines = require('../../../Common/sources/commondefines');
const config = require('../../../Common/node_modules/config');
const staticRouter = require('../../../DocService/sources/routes/static');

const cfgCacheStorage = config.get('storage');
const cfgPersistentStorage = utils.deepMergeObjects({}, cfgCacheStorage, config.get('persistentStorage'));

const ctx = operationContext.global;
const PORT = 3457;
const rand = Math.floor(Math.random() * 1000000);
const testDir = 'DocService-DocsCoServer-storage-' + rand;
const baseUrl = `http://localhost:${PORT}`;
const urlType = commonDefines.c_oAscUrlTypes.Session;
const testFile1 = testDir + '/test1.txt';
const testFile2 = testDir + '/test2.txt';
const testFile3 = testDir + '/test3.txt';
const testFile4 = testDir + '/test4.txt';
const specialDirCache = '';
const specialDirForgotten = 'forgotten';

console.debug(`testDir: ${testDir}`);

let server;

beforeAll(async () => {
  //start server to server static files generated by getSignedUrl
  const app = express();
  app.use('/', staticRouter);
  server = app.listen(PORT, () => {
    console.debug('listening on ' + PORT);
  });
});

afterAll(async () => {
  if (server) {
    await new Promise(resolve => server.close(resolve));
  }
});

function getStorageCfg(specialDir) {
  return specialDir ? cfgPersistentStorage : cfgCacheStorage;
}

function request(url) {
  return new Promise((resolve, reject) => {
    const module = url.startsWith('https') ? https : http;
    const req = module.get(url, response => {
      let data = '';
      response.on('data', _data => (data += _data));
      response.on('error', error => reject(error));
      response.on('end', () => resolve(data));
    });

    req.on('error', error => reject(error));
  });
}
function runTestForDir(ctx, isMultitenantMode, specialDir) {
  const oldMultitenantMode = tenantManager.isMultitenantMode();
  test('start listObjects', async () => {
    //todo set in all tests do not rely on test order
    tenantManager.setMultitenantMode(isMultitenantMode);

    const list = await storage.listObjects(ctx, testDir, specialDir);
    expect(list).toEqual([]);
  });
  test('putObject', async () => {
    const buffer = Buffer.from(testFileData1);
    const res = await storage.putObject(ctx, testFile1, buffer, buffer.length, specialDir);
    expect(res).toEqual(undefined);
    const list = await storage.listObjects(ctx, testDir, specialDir);
    expect(list.sort()).toEqual([testFile1].sort());
  });
  test('putObject-stream', async () => {
    const buffer = Buffer.from(testFileData2);
    const stream = Readable.from(buffer);
    const res = await storage.putObject(ctx, testFile2, stream, buffer.length, specialDir);
    expect(res).toEqual(undefined);
    const list = await storage.listObjects(ctx, testDir, specialDir);
    expect(list.sort()).toEqual([testFile1, testFile2].sort());
  });
  if ('storage-fs' === getStorageCfg(specialDir).name) {
    test('UploadObject', async () => {
      const res = await storage.uploadObject(ctx, testFile3, 'createReadStream.txt', specialDir);
      expect(res).toEqual(undefined);
      expect(cp).toHaveBeenCalled();
      const list = await storage.listObjects(ctx, testDir, specialDir);
      expect(list.sort()).toEqual([testFile1, testFile2, testFile3].sort());
    });
  } else {
    test('uploadObject', async () => {
      const readStream = Readable.from(testFileData3);
      readStream.size = testFileData3.length;
      const spy = jest.spyOn(fs, 'createReadStream').mockReturnValue(readStream);
      const res = await storage.uploadObject(ctx, testFile3, 'createReadStream.txt', specialDir);
      expect(res).toEqual(undefined);
      const list = await storage.listObjects(ctx, testDir, specialDir);
      expect(spy).toHaveBeenCalled();
      expect(list.sort()).toEqual([testFile1, testFile2, testFile3].sort());
      spy.mockRestore();
    });

    //todo fails with storage-s3
    test.skip('uploadObject - stream error handling', async () => {
      const streamErrorMessage = new Error('Test stream error');
      const mockStream = Readable.from(
        (async function* () {
          yield 'first chunk\n';
          await new Promise(r => setTimeout(r, 5));
          throw streamErrorMessage;
        })()
      );
      mockStream.size = 1024;

      const spy = jest.spyOn(fs, 'createReadStream').mockReturnValue(mockStream);
      // Verify that the uploadObject function rejects when the stream emits an error
      await expect(storage.uploadObject(ctx, 'test-error-file.txt', 'nonexistent.txt', specialDir)).rejects.toThrow(streamErrorMessage);

      spy.mockRestore();
    });

    test.skip('uploadObject - non-existent file handling', async () => {
      const nonExistentFile = 'definitely-does-not-exist-' + Date.now() + '.txt';
      // Verify the file actually doesn't exist
      expect(fs.existsSync(nonExistentFile)).toBe(false);
      // Verify that uploadObject properly handles and propagates the error
      await expect(storage.uploadObject(ctx, 'test-error-file.txt', nonExistentFile, specialDir)).rejects.toThrow(/ENOENT/);
    });
  }
  test('copyObject', async () => {
    const res = await storage.copyObject(ctx, testFile3, testFile4, specialDir, specialDir);
    expect(res).toEqual(undefined);
    // let buffer = Buffer.from(testFileData3);
    // await storage.putObject(ctx, testFile3, buffer, buffer.length, specialDir);
    const list = await storage.listObjects(ctx, testDir, specialDir);
    expect(list.sort()).toEqual([testFile1, testFile2, testFile3, testFile4].sort());
  });
  test('headObject', async () => {
    let output;
    output = await storage.headObject(ctx, testFile1, specialDir);
    expect(output).toMatchObject({ContentLength: testFileData1.length});

    output = await storage.headObject(ctx, testFile2, specialDir);
    expect(output).toMatchObject({ContentLength: testFileData2.length});

    output = await storage.headObject(ctx, testFile3, specialDir);
    expect(output).toMatchObject({ContentLength: testFileData3.length});

    output = await storage.headObject(ctx, testFile4, specialDir);
    expect(output).toMatchObject({ContentLength: testFileData4.length});
  });
  test('getObject', async () => {
    let output;
    output = await storage.getObject(ctx, testFile1, specialDir);
    expect(output.toString('utf8')).toEqual(testFileData1);

    output = await storage.getObject(ctx, testFile2, specialDir);
    expect(output.toString('utf8')).toEqual(testFileData2);

    output = await storage.getObject(ctx, testFile3, specialDir);
    expect(output.toString('utf8')).toEqual(testFileData3);

    output = await storage.getObject(ctx, testFile4, specialDir);
    expect(output.toString('utf8')).toEqual(testFileData4);
  });
  test('createReadStream', async () => {
    let output, outputText;

    output = await storage.createReadStream(ctx, testFile1, specialDir);
    expect(output.contentLength).toEqual(testFileData1.length);
    outputText = await utils.stream2Buffer(output.readStream);
    expect(outputText.toString('utf8')).toEqual(testFileData1);

    output = await storage.createReadStream(ctx, testFile2, specialDir);
    expect(output.contentLength).toEqual(testFileData2.length);
    outputText = await utils.stream2Buffer(output.readStream);
    expect(outputText.toString('utf8')).toEqual(testFileData2);

    output = await storage.createReadStream(ctx, testFile3, specialDir);
    expect(output.contentLength).toEqual(testFileData3.length);
    outputText = await utils.stream2Buffer(output.readStream);
    expect(outputText.toString('utf8')).toEqual(testFileData3);
  });
  test('getSignedUrl', async () => {
    let url, data;
    url = await storage.getSignedUrl(ctx, baseUrl, testFile1, urlType, undefined, undefined, specialDir);
    data = await request(url);
    expect(data).toEqual(testFileData1);

    url = await storage.getSignedUrl(ctx, baseUrl, testFile2, urlType, undefined, undefined, specialDir);
    data = await request(url);
    expect(data).toEqual(testFileData2);

    url = await storage.getSignedUrl(ctx, baseUrl, testFile3, urlType, undefined, undefined, specialDir);
    data = await request(url);
    expect(data).toEqual(testFileData3);

    url = await storage.getSignedUrl(ctx, baseUrl, testFile4, urlType, undefined, undefined, specialDir);
    data = await request(url);
    expect(data).toEqual(testFileData4);
  });
  test('getSignedUrls', async () => {
    const urls = await storage.getSignedUrls(ctx, baseUrl, testDir, urlType, undefined, specialDir);
    const data = [];
    for (const i in urls) {
      data.push(await request(urls[i]));
    }
    expect(data.sort()).toEqual([testFileData1, testFileData2, testFileData3, testFileData4].sort());
  });
  test('getSignedUrlsArrayByArray', async () => {
    const urls = await storage.getSignedUrlsArrayByArray(ctx, baseUrl, [testFile1, testFile2], urlType, specialDir);
    const data = [];
    for (let i = 0; i < urls.length; ++i) {
      data.push(await request(urls[i]));
    }
    expect(data.sort()).toEqual([testFileData1, testFileData2].sort());
  });
  test('getSignedUrlsByArray', async () => {
    const urls = await storage.getSignedUrlsByArray(ctx, baseUrl, [testFile3, testFile4], undefined, urlType, specialDir);
    const data = [];
    for (const i in urls) {
      data.push(await request(urls[i]));
    }
    expect(data.sort()).toEqual([testFileData3, testFileData4].sort());
  });
  test('getSignedUrl with direct URLs enabled', async () => {
    const buffer = Buffer.from(testFileData1);
    const res = await storage.putObject(ctx, testFile1, buffer, buffer.length, specialDirCache);
    expect(res).toEqual(undefined);

    const url = await storage.getSignedUrl(ctx, baseUrl, testFile1, urlType, undefined, undefined, specialDirCache, true);
    const data = await request(url);
    expect(data).toEqual(testFileData1);

    if (cfgCacheStorage.name !== 'storage-fs') {
      expect(url).toContain(cfgCacheStorage.endpoint);
      expect(url).toContain(cfgCacheStorage.bucketName);
    }
  });
  test('getSignedUrl with direct URLs disabled', async () => {
    const buffer = Buffer.from(testFileData1);
    const res = await storage.putObject(ctx, testFile1, buffer, buffer.length, specialDirCache);
    expect(res).toEqual(undefined);

    const url = await storage.getSignedUrl(ctx, baseUrl, testFile1, urlType, undefined, undefined, specialDirCache, false);
    const data = await request(url);
    expect(data).toEqual(testFileData1);

    expect(url).toContain('md5');
    expect(url).toContain('expires');
    expect(url).toContain(cfgCacheStorage.storageFolderName);
  });
  test('deleteObject', async () => {
    let list;
    list = await storage.listObjects(ctx, testDir, specialDir);
    expect(list.sort()).toEqual([testFile1, testFile2, testFile3, testFile4].sort());

    const res = await storage.deleteObject(ctx, testFile1, specialDir);
    expect(res).toEqual(undefined);

    list = await storage.listObjects(ctx, testDir, specialDir);
    expect(list.sort()).toEqual([testFile2, testFile3, testFile4].sort());
  });
  test('deletePath', async () => {
    let list;
    list = await storage.listObjects(ctx, testDir, specialDir);
    expect(list.sort()).toEqual([testFile2, testFile3, testFile4].sort());

    const res = await storage.deletePath(ctx, testDir, specialDir);
    expect(res).toEqual(undefined);

    list = await storage.listObjects(ctx, testDir, specialDir);
    expect(list.sort()).toEqual([].sort());

    tenantManager.setMultitenantMode(oldMultitenantMode);
  });
}

// Assumed, that server is already up.
describe('storage common dir', () => {
  runTestForDir(ctx, false, specialDirCache);
});

describe('storage forgotten dir', () => {
  runTestForDir(ctx, false, specialDirForgotten);
});

describe('storage common dir with tenants', () => {
  runTestForDir(ctx, true, specialDirCache);
});

describe('storage forgotten dir with tenants', () => {
  runTestForDir(ctx, true, specialDirForgotten);
});

describe('storage mix common and forgotten dir', () => {
  test('putObject', async () => {
    tenantManager.setMultitenantMode(false);

    let buffer = Buffer.from(testFileData1);
    const res = await storage.putObject(ctx, testFile1, buffer, buffer.length, specialDirCache);
    expect(res).toEqual(undefined);
    let list = await storage.listObjects(ctx, testDir, specialDirCache);
    expect(list.sort()).toEqual([testFile1].sort());

    buffer = Buffer.from(testFileData2);
    const res2 = await storage.putObject(ctx, testFile2, buffer, buffer.length, specialDirForgotten);
    expect(res2).toEqual(undefined);
    list = await storage.listObjects(ctx, testDir, specialDirForgotten);
    expect(list.sort()).toEqual([testFile2].sort());
  });

  test('copyPath', async () => {
    const res = await storage.copyPath(ctx, testDir, testDir, specialDirCache, specialDirForgotten);
    expect(res).toEqual(undefined);

    const list = await storage.listObjects(ctx, testDir, specialDirForgotten);
    expect(list.sort()).toEqual([testFile1, testFile2].sort());
  });
  test('copyObject', async () => {
    const res = await storage.copyObject(ctx, testFile2, testFile2, specialDirForgotten, specialDirCache);
    expect(res).toEqual(undefined);

    const list = await storage.listObjects(ctx, testDir, specialDirCache);
    expect(list.sort()).toEqual([testFile1, testFile2].sort());
  });

  test('deletePath', async () => {
    let list, res;
    res = await storage.deletePath(ctx, testDir, specialDirCache);
    expect(res).toEqual(undefined);

    list = await storage.listObjects(ctx, testDir, specialDirCache);
    expect(list.sort()).toEqual([].sort());

    res = await storage.deletePath(ctx, testDir, specialDirForgotten);
    expect(res).toEqual(undefined);

    list = await storage.listObjects(ctx, testDir, specialDirForgotten);
    expect(list.sort()).toEqual([].sort());
  });
});
